package algocity.modelo.mapa;

import algocity.modelo.azar.FuncionAzar;
import algocity.modelo.juego.Parametros;
import algocity.modelo.zona.Superficie;
import algocity.modelo.zona.SuperficieAgua;
import algocity.modelo.zona.SuperficieLlano;

import java.io.Serializable;

import static java.lang.Math.min;

public class Mapa
    implements Censable, Accesible, Serializable {

    // Las hectáreas se numeran como tradicionalmente se hace con la matrices, siendo la coordenada "x"
    // la correspondiente a las columnas y la coordenada "y" la correspondiente a las filas
    private Hectarea[][] hectareas;
    private FuncionAzar azar;
    private int poblacion;

    public Mapa(int hectareasHorizontales, int hectareasVerticales, FuncionAzar azar) {
        hectareas = new Hectarea[hectareasHorizontales][hectareasVerticales];
        this.azar = azar;
        this.poblacion = 0;
        initHectareas(hectareasHorizontales, hectareasVerticales);
    }

    /**
     * Realiza la operación indicada por el 'operador' sobre las manzanas que están a
     * un 'radio' del 'centro'
     * Es seguro pasa un 'radio' que esté más allá de los límites del mapa porque el
     * algoritmo busca estos límites y maneja la operación de 'operador' dentro de los límites
     * del mapa
     * @param radio radio de acción positivo
     * @param centro hectárea que es centro del cuadrado
     * @param operador operador sobre el cual trabaja la hectárea
     */
    public void operarEnElRadio(int radio, Loteable centro, Operador<Hectarea> operador)  {

        Posicion posicionCentro = centro.getPosicion();
        final int lado = 2 * radio + 1;

        // Se comienza en el vértice superior derecho
        final Iterador2DVirtual<Hectarea> iterator = iteratorVirtual(posicionCentro.getX() + radio,
                                                                     posicionCentro.getY() - radio);
        recorrerBordeEnDireccion(Iterador2DDireccion.IZQUIERDA, operador, lado, iterator); // Recorrer el borde superior
        recorrerBordeEnDireccion(Iterador2DDireccion.ABAJO, operador, lado, iterator);     // Recorrer el borde derecho
        recorrerBordeEnDireccion(Iterador2DDireccion.DERECHA, operador, lado, iterator);   // Recorrer el borde inferios
        recorrerBordeEnDireccion(Iterador2DDireccion.ARRIBA, operador, lado, iterator);    // Recorrer el borde inferior
    }

    private void recorrerBordeEnDireccion(Iterador2DDireccion direccion, Operador operador, int lado, Iterador2DVirtual<Hectarea> iterator) {
        try {
            for (int x = 0; x < lado - 1; x++) {
                operarEnHectarea(operador, iterator);
                iterator.moverHacia(direccion);
            }

            operarEnHectarea(operador, iterator);

        } catch (FueraDelMapaException e) {
            // ToDo si se tira esta excepción se trata de un error de lógica interna
        }
    }

    private void operarEnHectarea(Operador<Hectarea> operador, Iterador2DVirtual<Hectarea> iterator) {
        final Hectarea elementoActual = iterator.elementoActual();
        if (iterator.esObservableElementoActual()) {
            operador.operar(elementoActual);
        }
    }

    /**
     * Inicializa las hectareas
     * @param hectareasHorizontales  cant. de hectareas horizontales (X)
     * @param hectareasVerticales cant. de hectareas verticales (Y)
     */
    private void initHectareas(int hectareasHorizontales, int hectareasVerticales) {
        int k = 0;
        for(int i = 0; i < hectareasHorizontales; i++) {
            for( int j=0; j < hectareasVerticales; j++) {
                Superficie superficie = azar.nuevoValor((float)0, (float)1) < Parametros.UMBRAL_DE_TIERRA ?
                        SuperficieLlano.getInstance() :
                        SuperficieAgua.getInstance();
                hectareas[i][j] = new Hectarea(new Posicion(i,j), superficie);
            }
        }
    }


    public void scan(Operador<Hectarea> operador) {
        try {
            final Iterador2D<Hectarea> iteratorY = iterator(0, 0);
            while (iteratorY.hayMasHacia(Iterador2DDireccion.ABAJO)) {
                scanFila(iteratorY, operador);
                iteratorY.moverHacia(Iterador2DDireccion.ABAJO);
            };
            scanFila(iteratorY, operador);
 
        } catch (FueraDelMapaException e) {
            // ToDo no debe ocurrir, lógica errónea
        }
    }

    private void scanFila(Iterador2D<Hectarea> iteratorY, Operador<Hectarea> operador) throws FueraDelMapaException {
        final Iterador2D<Hectarea> iteratorX = iterator(0,
                iteratorY.elementoActual().getPosicion().getY());

        //      quitarla de la lista una vez que está reparada
        while (iteratorX.hayMasHacia(Iterador2DDireccion.DERECHA))  {
            operar(iteratorX, operador);
            iteratorX.moverHacia(Iterador2DDireccion.DERECHA);
        }

        operar(iteratorX, operador);
    }

    private void operar(Iterador2D<Hectarea> iterator, Operador<Hectarea> operador) throws FueraDelMapaException {
        final Hectarea hectarea = iterator.elementoActual();
        operador.operar(hectarea);
    }


    public int getPoblacion() {
        return poblacion;
    }

    @Override
    public void setPoblacion(int poblacion) {
        this.poblacion = poblacion;
    }

    public Iterador2D<Hectarea> iterator(int x, int y) {
        return new MapaIterador(x, y);
    }


    private Iterador2DVirtual<Hectarea> iteratorVirtual(int x, int y) {
        return new MapaIteradorVirtual(x, y);
    }

    public Iterador2D<Hectarea> iterator(Posicion posicion) {
        return new MapaIterador(posicion.getX(), posicion.getY());
    }


    public Hectarea hectareaDeBordeAlAzar() {
        /*
            Invariante de hectárea :
                si X está en uno de sus valores extremos
                    entonces Y puede ser cualquier valor dentro del rango de hectáreas vertcales
                si Y está en uno de sus valores extremos
                    entonces X puede ser cualquier valor dentro del rango de hectáreas horizontales
         */
        Posicion posicionDeBorde = calcularPosicionDeBorde();
        return hectareas[posicionDeBorde.getX()][posicionDeBorde.getY()];
    }

    private Posicion calcularPosicionDeBorde() {
        boolean xEsExtremo = azar.nuevoBoolean();

        if (xEsExtremo) {
            int posicionX = azar.nuevoValorExtremo( 0, hectareas.length - 1 );
            return new Posicion(posicionX, getYAlAzar());
        } else {
            int posicionY = azar.nuevoValorExtremo( 0, hectareas[0].length - 1);
            return new Posicion(getXAlAzar(), posicionY);
        }
    }

    public Hectarea hectareaAlAzar() {
        return hectareas[getXAlAzar()][getYAlAzar()];
    }

    public Hectarea hectareaAlAzarLibre() {
        Hectarea hectarea = hectareaAlAzar();
        while( ! hectarea.esTerrenoBaldio() ) {
            hectarea = hectareaAlAzar();
        }

        return hectarea;
    }

    private int getXAlAzar() {
        return ajustarAzar(hectareas.length);
    }

    private int getYAlAzar() {
        return ajustarAzar(hectareas[0].length);
    }

    private int ajustarAzar(int max) {
        final int y = azar.nuevoValor(0, max);
        return min(y, max);
    }

    protected boolean enRango(Posicion proximo) {
        return enRango(proximo.getX(), hectareas.length) &&
                enRango(proximo.getY(), hectareas[0].length);
    }

    private boolean enRango(Integer indice, Integer rango) {
        return indice < rango && indice >= 0;
    }

    /**
     * Dimensiones del mapa, cantidad de hectáreas de ancho y de largo
     * @return diemnsión del mapa
     */
    public Dimension getDimensiones() {
        return new Dimension(hectareas.length, hectareas[0].length);
    }


    public boolean equals(Object otroObjeto) {
        if (this == otroObjeto) {
            return true;
        }

        if (!(otroObjeto instanceof Mapa)) {
            return false;
        }

        Mapa otroMapa = (Mapa) otroObjeto;
        return ( poblacion == otroMapa.poblacion ) &&
                equalsHectareas(otroMapa);
    }

    private boolean equalsHectareas(Mapa otroMapa) {
        return hectareas.length == otroMapa.hectareas.length &&
                hectareas[0].length == otroMapa.hectareas[0].length &&
                equalsCadaHectarea(otroMapa);
    }

    private boolean equalsCadaHectarea(Mapa otroMapa) {
        for ( int x = 0; x < hectareas.length; x++) {
            for ( int y = 0; y < hectareas[0].length; y++) {
                if ( ! hectareas[x][y].equals(otroMapa.hectareas[x][y])) {
                    return false;
                }
            }
        }
        return true;
    }

    private class MapaIterador implements Iterador2D<Hectarea> {
        private int y;
        private int x;

        public MapaIterador(int x, int y) {
            this.x = x;
            this.y = y;
        }

        @Override
        public Hectarea elementoActual()  {
            return hectareas[x][y];
        }

        @Override
        public void moverHacia(Iterador2DDireccion direccion) throws FueraDelMapaException {
            final Posicion desplazamiento = direccion.desplazamiento(x, y);

            if ( ! enRango(desplazamiento))  {
                throw new FueraDelMapaException(desplazamiento);
            }

            x = desplazamiento.getX();
            y = desplazamiento.getY();
        }

        @Override
        public boolean hayMasHacia(Iterador2DDireccion direccion) {
            final Posicion proximo = direccion.desplazamiento(x, y);
            return enRango(proximo);
        }

    }

    private class MapaIteradorVirtual
        implements Iterador2DVirtual<Hectarea> {

        private int xVirtual;
        private int yVirtual;
        private boolean esObservableElementoActual;

        public MapaIteradorVirtual(int xVirtual, int yVirtual) {
            this.xVirtual = xVirtual;
            this.yVirtual = yVirtual;
        }

        public Hectarea elementoActual() {
            esObservableElementoActual = enRango(xVirtual, hectareas.length) &&
                                         enRango(yVirtual, hectareas[0].length);
            if (esObservableElementoActual) {
                return hectareas[xVirtual][yVirtual];
            } else {
                // Se devuelve una hectárea ficticia.
                return new Hectarea(new Posicion(xVirtual, yVirtual), SuperficieLlano.getInstance());
            }
        }

        public void moverHacia(Iterador2DDireccion direccion) throws FueraDelMapaException {
            final Posicion desplazamiento = direccion.desplazamiento(xVirtual, yVirtual);
            xVirtual = desplazamiento.getX();
            yVirtual = desplazamiento.getY();
        }

        public boolean hayMasHacia(Iterador2DDireccion direccion) {
            return true;
        }

        @Override
        public boolean esObservableElementoActual() {
            return this.esObservableElementoActual;
        }

        }


}
